﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using CoreEx.Common.Extensions;
using CoreEx.Common.Linq;
using CoreEx.Common.Validation;
using LinFu.IoC.Configuration;

namespace CoreEx.Common.Implementation.Validation
{
    /// <summary>
    /// Rewrites the expression tree in such 
    /// as way that member expressions don't return <c>null</c> values unless an explicit
    /// check for <c>null</c> is intended.
    /// </summary>
    [Implements(typeof(IRuleRewriter))]
    public class RuleRewriter : ExpressionVisitor, IRuleRewriter
    {
        private readonly IList<MemberExpression> _excludeList = new List<MemberExpression>();
        private IEnumerable<PropertyInfo> _targetProperties;

        #region IRuleRewriter Members

        /// <summary>
        /// Rewrites the <see cref="ruleExpression"/> by replacing any <see cref="MemberExpression"/> with a <see cref="ConditionalExpression"/>
        /// where the member is not explicitly checked for <c>null</c>.
        /// </summary>
        /// <typeparam name="T">The target type to validate.</typeparam>
        /// <param name="ruleExpression">The validation expression.</param>
        /// <param name="targetProperties">The target properties for the <paramref name="ruleExpression"/> that are candidates for rewriting.</param>
        /// <returns><see cref="Expression{TDelegate}"/></returns>
        public Expression<Func<T, bool>> Rewrite<T>(Expression<Func<T, bool>> ruleExpression,
                                                    IEnumerable<PropertyInfo> targetProperties)
        {
            _targetProperties = targetProperties;
            return (Expression<Func<T, bool>>) Visit(ruleExpression);
        }

        #endregion

        /// <summary>
        /// Check to see if a <see cref="MemberExpression"/> is part of an explicit check for <c>null</c>
        /// </summary>
        /// <param name="binaryExpression">The currently visited <see cref="BinaryExpression"/></param>
        /// <returns><see cref="BinaryExpression"/></returns>
        protected override Expression VisitBinary(BinaryExpression binaryExpression)
        {
            if (IsExplicitCheckForNull(binaryExpression))
                ExcludeFromMemberRewrite(binaryExpression);
            return base.VisitBinary(binaryExpression);
        }

        /// <summary>
        /// Replaces the <see cref="MemberExpression"/> with a <see cref="ConditionalExpression"/>
        /// that return the default value if the member yields null.
        /// </summary>
        /// <param name="memberExpression">The currently visited <see cref="MemberExpression"/></param>
        /// <returns>A <see cref="ConditionalExpression"/> if a rewrite has been performed, otherwise the original <see cref="MemberExpression"/></returns>
        protected override Expression VisitMemberAccess(MemberExpression memberExpression)
        {
            if (ShouldRewiteMemberExpression(memberExpression))
                return RewriteMemberExpression(memberExpression);

            return base.VisitMemberAccess(memberExpression);
        }

        private bool ShouldRewiteMemberExpression(MemberExpression memberExpression)
        {
            if (!IsTargetProperty(memberExpression) || IsMemberExcluded(memberExpression))
                return false;
            return MemberIsReferenceTypeOrNullableValueType(memberExpression);
        }

        private bool MemberIsReferenceTypeOrNullableValueType(MemberExpression memberExpression)
        {
            var propertyInfo = (PropertyInfo) memberExpression.Member;
            return !propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType.IsNullableType();
        }


        private Expression RewriteMemberExpression(MemberExpression memberExpression)
        {
            var propertyInfo = (PropertyInfo) memberExpression.Member;
            return Expression.Condition(GetTestExpression(memberExpression), GetDefaultValueExpression(propertyInfo),
                                        memberExpression);
        }

        private static BinaryExpression GetTestExpression(MemberExpression memberExpression)
        {
            return Expression.Equal(memberExpression, Expression.Constant(null));
        }

        private static UnaryExpression GetDefaultValueExpression(PropertyInfo propertyInfo)
        {
            return Expression.Convert(Expression.Constant(GetDefaultValue(propertyInfo.PropertyType)),
                                      propertyInfo.PropertyType);
        }


        private void ExcludeFromMemberRewrite(BinaryExpression binaryExpression)
        {
            if (IsTargetProperty(binaryExpression.Left))
                _excludeList.Add((MemberExpression) binaryExpression.Left);
            if (IsTargetProperty(binaryExpression.Right))
                _excludeList.Add((MemberExpression) binaryExpression.Right);
        }

        private bool IsMemberExcluded(MemberExpression memberExpression)
        {
            return _excludeList.Contains(memberExpression);
        }


        private bool IsExplicitCheckForNull(BinaryExpression binaryExpression)
        {
            if (IsNullConstant(binaryExpression.Left) && IsTargetProperty(binaryExpression.Right))
                return true;
            if (IsNullConstant(binaryExpression.Right) && IsTargetProperty(binaryExpression.Left))
                return true;
            return false;
        }


        private static bool IsNullConstant(Expression expression)
        {
            var constantExpression = (expression as ConstantExpression);
            if (constantExpression == null)
                return false;
            return (constantExpression.Value == null);
        }

        private bool IsTargetProperty(Expression expression)
        {
            var memberExpression = (expression as MemberExpression);
            if (memberExpression == null)
                return false;
            var propertyInfo = (PropertyInfo) memberExpression.Member;
            if (propertyInfo == null)
                return false;
            return _targetProperties.Contains(propertyInfo);
        }


        /// <summary>
        /// Gets the default value to be used as a surrogate for the supplied <c>null</c> value.
        /// </summary>
        /// <param name="type">The target type.</param>
        /// <returns>The default value as determined by the underlying value type.</returns>
        private static object GetDefaultValue(Type type)
        {
            //We need some special handling for the string data type 
            //since it is a reference type, but behaves like a value type.
            if (type == typeof (string))
                return string.Empty;

            return type.GetNonNullableType().GetDefaultValue();
        }
    }
}